package org.apache.ftpserver.ext;

import java.io.IOException;
import java.net.ServerSocket;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.StringTokenizer;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class PassivePorts {
	 private Logger log = LoggerFactory.getLogger(PassivePorts.class);

	    private static final int MAX_PORT = 65535;

	    private static final Integer MAX_PORT_INTEGER = Integer.valueOf(MAX_PORT);

	    private List<Integer> freeList;

	    private Set<Integer> usedList;

	    private Random r = new Random();

	    private String passivePortsString;

	    private boolean checkIfBound;

	    /**
	     * Parse a string containing passive ports
	     * 
	     * @param portsString
	     *            A string of passive ports, can contain a single port (as an
	     *            integer), multiple ports seperated by commas (e.g.
	     *            123,124,125) or ranges of ports, including open ended ranges
	     *            (e.g. 123-125, 30000-, -1023). Combinations for single ports
	     *            and ranges is also supported.
	     * @return A list of Integer objects, based on the parsed string
	     * @throws IllegalArgumentException
	     *             If any of of the ports in the string is invalid (e.g. not an
	     *             integer or too large for a port number)
	     */
	    private static Set<Integer> parse(final String portsString) {
	        Set<Integer> passivePortsList = new HashSet<Integer>();

	        boolean inRange = false;
	        Integer lastPort = Integer.valueOf(1);
	        StringTokenizer st = new StringTokenizer(portsString, ",;-", true);
	        while (st.hasMoreTokens()) {
	            String token = st.nextToken().trim();

	            if (",".equals(token) || ";".equals(token)) {
	                if (inRange) {
	                    fillRange(passivePortsList, lastPort, MAX_PORT_INTEGER);
	                }

	                // reset state
	                lastPort = Integer.valueOf(1);
	                inRange = false;
	            } else if ("-".equals(token)) {
	                inRange = true;
	            } else if (token.length() == 0) {
	                // ignore whitespace
	            } else {
	                Integer port = Integer.valueOf(token);

	                verifyPort(port);

	                if (inRange) {
	                    // add all numbers from last int
	                    fillRange(passivePortsList, lastPort, port);

	                    inRange = false;
	                }

	                addPort(passivePortsList, port);

	                lastPort = port;
	            }
	        }

	        if (inRange) {
	            fillRange(passivePortsList, lastPort, MAX_PORT_INTEGER);
	        }

	        return passivePortsList;
	    }

	    /**
	     * Fill a range of ports
	     */
	    private static void fillRange(final Set<Integer> passivePortsList, final Integer beginPort, final Integer endPort) {
	        for (int i = beginPort; i <= endPort; i++) {
	            addPort(passivePortsList, Integer.valueOf(i));
	        }
	    }

	    /**
	     * Add a single port if not already in list
	     */
	    private static void addPort(final Set<Integer> passivePortsList, final Integer port) {
	        passivePortsList.add(port);
	    }

	    /**
	     * Verify that the port is within the range of allowed ports
	     */
	    private static void verifyPort(final int port) {
	        if (port < 0) {
	            throw new IllegalArgumentException("Port can not be negative: " + port);
	        } else if (port > MAX_PORT) {
	            throw new IllegalArgumentException("Port too large: " + port);
	        }
	    }

	    public PassivePorts(final String passivePorts, boolean checkIfBound) {
	        this(parse(passivePorts), checkIfBound);

	        this.passivePortsString = passivePorts;
	    }

	    public PassivePorts(Set<Integer> passivePorts, boolean checkIfBound) {
	        if (passivePorts == null) {
	            throw new NullPointerException("passivePorts can not be null");
	        } else if(passivePorts.isEmpty()) {
	        	passivePorts = new HashSet<Integer>();
	        	passivePorts.add(0);
	        }

	        this.freeList = new ArrayList<Integer>(passivePorts);
	        this.usedList = new HashSet<Integer>(passivePorts.size());

	        this.checkIfBound = checkIfBound;
	    }

	    /**
	     * Checks that the port of not bound by another application
	     */
	    private boolean checkPortUnbound(int port) {
	        // is this check disabled?
	        if (!checkIfBound) {
	            return true;
	        }

	        // if using 0 port, it will always be available
	        if (port == 0) {
	            return true;
	        }

	        ServerSocket ss = null;
	        try {
	            ss = new ServerSocket(port);
	            ss.setReuseAddress(true);
	            return true;
	        } catch (IOException e) {
	            // port probably in use, check next
	            return false;
	        } finally {
	            if (ss != null) {
	                try {
	                    ss.close();
	                } catch (IOException e) {
	                    // could not close, check next
	                    return false;
	                }
	            }
	        }
	    }

	    public synchronized int reserveNextPort() {
	    	// create a copy of the free ports, so that we can keep track of the tested ports
	    	List<Integer> freeCopy = new ArrayList<Integer>(freeList);
	    	
	        // Loop until we have found a port, or exhausted all available ports
	        while (freeCopy.size() > 0) {
	            // Otherwise, pick one at random
	            int i = r.nextInt(freeCopy.size());
	            Integer ret = freeCopy.get(i);

	            if (ret == 0) {
	                // "Any" port should not be removed from our free list,
	                // nor added to the used list
	                return 0;

	            } else if (checkPortUnbound(ret)) {
	                // Not used by someone else, so lets reserve it and return it
	                freeList.remove(i);
	                usedList.add(ret);
	                return ret;

	            } else {
	            	freeCopy.remove(i);
	                // log port unavailable, but left in pool
	                log.warn("Passive port in use by another process: " + ret);
	            }
	        }

	        return -1;
	    }

	    public synchronized void releasePort(final int port) {
	        if (port == 0) {
	            // Ignore port 0 being released,
	            // since its not put on the used list

	        } else if (usedList.remove(port)) {
	            freeList.add(port);

	        } else {
	            // log attempt to release unused port
	            log.warn("Releasing unreserved passive port: " + port);
	        }
	    }

	    @Override
	    public String toString() {
	        if (passivePortsString != null) {
	            return passivePortsString;
	        }

	        StringBuilder sb = new StringBuilder();

	        for (Integer port : freeList) {
	            sb.append(port);
	            sb.append(",");
	        }
	        // remove the last ,
	        sb.deleteCharAt(sb.length() - 1);
	        return sb.toString();
	    }
}
